// Author: wsfuyibing <682805@qq.com>
// Date: 2025-04-28

package weixin

import (
    "context"
    "encoding/json"
    "fmt"
    "gitee.com/go-libs/log"
    "gitee.com/go-libs/redis"
    "github.com/valyala/fasthttp"
    "net/http"
    "server/app/config"
    "server/app/kind"
    "server/app/lib/weixin/responses"
)

type (
    Manager interface {
        AccessToken(ctx context.Context, forces ...bool) (token string, err error)
        GetMobile(ctx context.Context, code string, forces ...bool) (mobile kind.Mobile, err error)
        GetOpenId(ctx context.Context, code string) (str string, err error)
        GetUserInfo(ctx context.Context, openid string) (avatar, nickname string, err error)
    }

    manager struct {
    }
)

func New() Manager {
    return (&manager{}).init()
}

// AccessToken
// 获取小程序令牌.
//
// @doc https://developers.weixin.qq.com/miniprogram/dev/OpenApiDoc/mp-access-token/getAccessToken.html
func (o *manager) AccessToken(ctx context.Context, forces ...bool) (token string, err error) {
    var (
        expires int
        force   = false
        key     = fmt.Sprintf(`weixin:token:%s`, config.WeiXin.GetAppId())
        res     redis.Result
    )

    // 1. 强制刷新.
    if len(forces) > 0 && forces[0] {
        force = true
    }

    // 2. 读取缓存
    if !force {
        // 2.1 内部错误.
        if res = redis.Get(ctx, key); res.Error() != nil {
            err = res.Error()
            return
        }

        // 2.2 复用缓存.
        if token = res.String(); token != "" {
            log.Debugfc(ctx, `[weixin] reuse token: token="%s"`, token)
            return
        }
    }

    // 3. 请求令牌.
    if token, expires, err = o.tokenizer(ctx); err == nil {
        // 3.1 保存令牌.
        if res = redis.SetEx(ctx, key, token, expires-180); res.Error() != nil {
            err = res.Error()
        }
    }
    return
}

// GetMobile
// 获取用户手机号.
//
// @doc https://developers.weixin.qq.com/miniprogram/dev/OpenApiDoc/user-info/phone-number/getPhoneNumber.html
func (o *manager) GetMobile(ctx context.Context, code string, forces ...bool) (mobile kind.Mobile, err error) {
    var (
        body  []byte
        res   *responses.MobileResponse
        token string
    )

    if token, err = o.AccessToken(ctx, forces...); err != nil {
        return
    }

    if body, err = o.send(ctx, fmt.Sprintf(`%s/wxa/business/getuserphonenumber?access_token=%s`,
        config.WeiXin.GetAppHost(),
        token,
    ), http.MethodPost, []byte(fmt.Sprintf(`{"code":"%s"}`, code))); err == nil {
        res = &responses.MobileResponse{}
        if err = json.Unmarshal(body, res); err == nil {
            if res.Code == 0 {
                mobile = kind.Mobile(res.Info.PhoneNumber)
                return
            }

            if res.Code == 40001 {
                return o.GetMobile(ctx, code, true)
            }

            err = fmt.Errorf(`%d: %v`, res.Code, res.Message)
        }
    }
    return
}

// GetOpenId
// 获取用户OpenId值.
//
// @doc https://developers.weixin.qq.com/miniprogram/dev/OpenApiDoc/user-login/code2Session.html
func (o *manager) GetOpenId(ctx context.Context, code string) (str string, err error) {
    var (
        body []byte
        res  *responses.OpenidResponse
    )
    if body, err = o.send(ctx, fmt.Sprintf(`%s/sns/jscode2session?appid=%s&secret=%s&js_code=%s&grant_type=authorization_code`,
        config.WeiXin.GetAppHost(),
        config.WeiXin.GetAppId(),
        config.WeiXin.GetAppSecret(),
        code,
    ), http.MethodGet, nil); err == nil {
        res = &responses.OpenidResponse{}
        if err = json.Unmarshal(body, res); err == nil {
            if res.Code == 0 {
                str = res.OpenId
                return
            }
            err = fmt.Errorf(`%d: %v`, res.Code, res.Message)
        }
    }
    return
}

func (o *manager) GetUserInfo(ctx context.Context, openid string) (avatar, nickname string, err error) {
    var (
        body  []byte
        res   *responses.UserInfoResponse
        token string
    )

    if token, err = o.AccessToken(ctx); err != nil {
        return
    }

    if body, err = o.send(ctx, fmt.Sprintf(`%s/cgi-bin/component/api_get_authorizer_info?access_token=%s`,
        config.WeiXin.GetAppHost(),
        token,
    ), http.MethodPost, []byte(fmt.Sprintf(`{"component_appid":"%s","authorizer_appid":"%s"}`, openid, config.WeiXin.GetAppId()))); err == nil {
        res = &responses.UserInfoResponse{}
        if err = json.Unmarshal(body, res); err == nil {
            if res.Code == 0 {
                avatar = res.Info.Avatar
                nickname = res.Info.Nickname
                return
            }
            err = fmt.Errorf(`%d: %v`, res.Code, res.Message)
        }
    }
    return
}

func (o *manager) init() *manager {
    return o
}

func (o *manager) send(ctx context.Context, url, method string, data []byte) (body []byte, err error) {
    var (
        request  = fasthttp.AcquireRequest()
        response = fasthttp.AcquireResponse()
    )

    // 1. 释放资源.
    //    请求结束后释放资源, 同时捕获未知异常.
    defer func() {
        // 捕获异常.
        if r := recover(); r != nil {
            log.Fatalfc(ctx, `[weixin] http request caused an panic: fatal="%v"`, r)

            if err == nil {
                err = fmt.Errorf(`%v`, r)
            }
        }

        // 释放资源.
        fasthttp.ReleaseRequest(request)
        fasthttp.ReleaseResponse(response)

        log.Infofc(ctx, `[weixin] http request end: body="%s"`, body)
    }()

    log.Infofc(ctx, `[weixin] http request begin: method="%s" url="%s"`, method, url)

    // 2. 准备请求.
    request.Header.SetMethod(method)
    request.Header.SetContentType(config.WeiXin.GetContentType())
    request.Header.SetRequestURI(url)

    // 3. 请求数据.
    if data != nil {
        request.SetBody(data)
    }

    // 4. 请求链路.
    for k, v := range log.ContextHeader(ctx) {
        if len(v) > 0 && v[0] != "" {
            request.Header.Set(k, v[0])
        }
    }

    // 5. 请求过程.
    if err = fasthttp.DoTimeout(request, response, config.WeiXin.GetTimeoutDuration()); err != nil {
        return
    }

    // 6. 状态校验.
    if status := response.StatusCode(); status != http.StatusOK {
        err = fmt.Errorf(`HTTP %d %s`, status, http.StatusText(status))
        return
    }

    // 7. 请求完成.
    body = response.Body()
    return
}

func (o *manager) tokenizer(ctx context.Context) (token string, expires int, err error) {
    var (
        body []byte
        res  *responses.AccessTokenResponse
    )
    if body, err = o.send(ctx, fmt.Sprintf(`%s/cgi-bin/token?grant_type=client_credential&appid=%s&secret=%s`,
        config.WeiXin.GetAppHost(),
        config.WeiXin.GetAppId(),
        config.WeiXin.GetAppSecret(),
    ), http.MethodGet, nil); err == nil {
        res = &responses.AccessTokenResponse{}
        if err = json.Unmarshal(body, res); err == nil {
            if res.Code == 0 {
                token = res.AccessToken
                expires = res.ExpiresIn
                return
            }
            err = fmt.Errorf(`%d: %v`, res.Code, res.Message)
        }
    }
    return
}
